package main;

import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.PriorityQueue;

import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;

import model.ACK;
import model.BackgroundTraffic;
import model.Packet;
import model.Router;
import model.RouterFIFO;
import model.RouterRED;
import model.RxTCP;
import model.RxTCP1;
import model.RxTCP2;
import model.TxTCP;
import model.TxTCP1;
import model.TxTCP2;
import statistics.CwndMssStatistics;
import statistics.ICVazaoMediaStatistics;
import statistics.Statistics;
import statistics.VazaoMediaStatistics;
import enums.QueueType;
import event.ACKReceivedEvent;
import event.ACKTimeoutEvent;
import event.BackgroundTrafficEvent;
import event.Event;
import event.FinishSimulationEvent;
import event.FinishTransienteEvent;
import event.ICVazaoMediaEvent;
import event.PacketArrivedAtRouterEvent;
import event.PacketReceivedByReceiverEvent;
import event.PacketSentByRouterEvent;

public class Simulator {

	private long startRealTime = System.currentTimeMillis();
	private double time;
	private PriorityQueue<Event> events;
	private Router router;
	private List<TxTCP> transmitters;
	private List<RxTCP> receivers;
	private BackgroundTraffic backTraffic;
	private SimulatorProperties properties;
	
	List<Statistics> stats = new ArrayList<Statistics>();
	ICVazaoMediaStatistics icStats;
	
	public static void main(String[] args) {
		JPanel panel = new JPanel();
		JFileChooser fc = new JFileChooser();
		if (fc.showOpenDialog(panel) == JFileChooser.APPROVE_OPTION) {
			File file = fc.getSelectedFile();
			SimulatorProperties prop = new SimulatorProperties(file);
			prop.setPathToSave(file.getParent());
			Simulator simulator = new Simulator(prop);
			
			JFrame jFrame = new JFrame();
			JOptionPane.showMessageDialog(jFrame, "Clique em OK para iniciar e aguarde a mensagem de sucesso" +
					" indicando que simulação terminou corretamente. " +
					"\n\nOs arquivos de saída da simulação serão escritos no mesmo diretório onde está localizado o properties selecionado.");
			simulator.run();
			JOptionPane.showMessageDialog(jFrame, "Simulação finalizada com sucesso.");
			jFrame.dispose();
		}
	}
	
	public Simulator(SimulatorProperties properties, Long initSessionsInterval) {
		init(properties, initSessionsInterval);
	}
	
	public Simulator(SimulatorProperties properties) {
		init(properties, 0l);
	}

	private void init(SimulatorProperties properties, Long initSessionsInterval) {
		this.properties = properties;
		time = 0;
		events = new PriorityQueue<Event>();
		
		int bufferCapacity = properties.getBufferCapacity();
		if (properties.getQueueType().equals(QueueType.FIFO.toString())) {
			router = new RouterFIFO(this, bufferCapacity);
		} else if (properties.getQueueType().equals(QueueType.RED.toString())) {
			router = new RouterRED(this, bufferCapacity);
		} else {
			throw new IllegalArgumentException("Política de descarte do roteador inválido.");
		}
		
		transmitters = new ArrayList<TxTCP>();
		receivers = new ArrayList<RxTCP>();
		backTraffic = new BackgroundTraffic(this);
		
		int n1 = properties.getN1();
		int n2 = properties.getN2();
		int n = n1 + n2;
		
		for (int sessionId = 0; sessionId < n1; sessionId++) {
			transmitters.add(new TxTCP1(this, sessionId));
			receivers.add(new RxTCP1(this, sessionId));
			double startTime = RandomGenerator.nextDouble() * initSessionsInterval;
			Packet pack = new Packet(sessionId, 0l);
			transmitters.get(sessionId).sendPacket(pack, startTime);
		}
		for (int sessionId = n1; sessionId < n; sessionId++) {
			transmitters.add(new TxTCP2(this, sessionId));
			receivers.add(new RxTCP2(this, sessionId));
			double startTime = RandomGenerator.nextDouble() * initSessionsInterval;
			Packet pack = new Packet(sessionId, 0l);
			transmitters.get(sessionId).sendPacket(pack, startTime);
		}
		
		stats.add(new CwndMssStatistics(this, properties.getName(), "tempo (ms)", "txwnd/mss", n));
		stats.add(new CwndMssStatistics(this, properties.getName(), "tempo (ms)", "txwnd/mss", n));
		stats.add(new VazaoMediaStatistics(this, properties.getName(), "tempo (ms)", "pacotes/s", n));
		
		addEvent(new FinishTransienteEvent(properties.getTransiente()));
		
		if (properties.getBackTraffic()) {
			addEvent(new BackgroundTrafficEvent(initSessionsInterval));
		}
	}
	
	public double getTime() {
		return time;
	}
	
	public Router getRouter() {
		return router;
	}

	public List<TxTCP> getTransmitters() {
		return transmitters;
	}
	
	public List<RxTCP> getReceivers() {
		return receivers;
	}

	public SimulatorProperties getProperties() {
		return properties;
	}
	
	public void addEvent(Event event) {
		events.add(event);
	}

	public void removeEvent(Event event) {
		events.remove(event);
	}

	public void removeTimeoutEvents(int sessionId) {
		Iterator<Event> iterator = events.iterator();
		while (iterator.hasNext()) {
			Event event = iterator.next();
			if (event instanceof ACKTimeoutEvent) {
				if (((ACKTimeoutEvent) event).getPack().getSessionId() == sessionId) {
					iterator.remove();
				}
			}
		}
	}
	
	public void run() {
		while (!events.element().getClass().equals(FinishSimulationEvent.class)) {
			processEvent(events.remove());
		}
		for (Statistics stat : stats) {
			stat.createChart();
			stat.saveAsFile();
		}
		icStats.saveAsFile();
	}
	
	public void processEvent(Event event) {
		time = event.getTime();
		Class<? extends Event> eventClass = event.getClass();
		if (eventClass.equals(PacketArrivedAtRouterEvent.class)) {
			Packet pack = ((PacketArrivedAtRouterEvent) event).getPack();
			router.packetArrived(pack);
		} else if (eventClass.equals(PacketSentByRouterEvent.class)) {
			Packet pack = ((PacketSentByRouterEvent) event).getPack();
			router.sendPacket(pack);
		} else if (eventClass.equals(PacketReceivedByReceiverEvent.class)) {
			Packet pack = ((PacketReceivedByReceiverEvent) event).getPack();
			receivers.get(pack.getSessionId()).receivePacket(pack);
		} else if (eventClass.equals(ACKReceivedEvent.class)) {
			ACK ack = ((ACKReceivedEvent) event).getACK();
			transmitters.get(ack.getPack().getSessionId()).receiveACK(ack);
		} else if (eventClass.equals(ACKTimeoutEvent.class)) {
			Packet pack = ((ACKTimeoutEvent) event).getPack();
			transmitters.get(pack.getSessionId()).handleTimeout(pack);
		} else if (eventClass.equals(BackgroundTrafficEvent.class)) {
			backTraffic.sendPackets();
		} else if (eventClass.equals(ICVazaoMediaEvent.class)) {
			icStats.collect();
		} else if (eventClass.equals(FinishTransienteEvent.class)) {
			icStats = new ICVazaoMediaStatistics(this);
			addEvent(new ICVazaoMediaEvent(getTime() + getProperties().getIntervaloEntreColetas()));
		} else {
			throw new IllegalArgumentException("Evento não reconhecido.");
		}
		
		for (Statistics stat : stats) {
			stat.collect(event);
		}
	}

	public long getStartRealTime() {
		return startRealTime;
	}

	public void setStartRealTime(long startRealTime) {
		this.startRealTime = startRealTime;
	}
	
}
